# coding: utf-8
from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Request
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
import requests
import json
import time
import os
from dotenv import load_dotenv
import pandas as pd
from io import BytesIO
import uuid
import re
import asyncio
import threading

load_dotenv()

# 从环境变量读取超时时间，默认10分钟(600秒)
DEFAULT_TIMEOUT = int(os.getenv("LANGFLOW_TIMEOUT", "600"))

app = FastAPI(title="LangFlow API Tester")
templates = Jinja2Templates(directory="templates")

DEFAULT_URL = "http://192.168.200.19:57868/api/v1/run/emr-text2json"
DEFAULT_SESSION_ID = "python测试：001"
DEFAULT_CONTAINER_NAME = "SchemaBlueprint"
DEFAULT_TWEAKS = {
    DEFAULT_CONTAINER_NAME: {
        "my_json_schema": {
            "type": "NestedDict",
            "value": {
                "schema": {
                    "type": "object",
                    "properties": {
                        "姓名": {"type": "string", "description": "患者姓名"},
                        "婚否": {"type": "string", "description": "患者是否已婚,['已婚' / '未婚']"}
                    },
                    "required": ["姓名"],
                    "additionalProperties": False
                },
                "name": "jsonEntity",
                "strict": True
            }
        }
    }
}

HTML_TEMPLATE = """
<!doctype html>
<html lang=\"zh-CN\">
<head>
<meta charset=\"utf-8\" />
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
<title>LangFlow API Tester</title>
<style>
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'PingFang SC', 'Microsoft YaHei', sans-serif; margin: 0; padding: 0; background: #f6f7f9; }
  .container { max-width: 1380px; margin: 24px auto; background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.04); }
  header { padding: 16px 20px; border-bottom: 1px solid #eef2f7; }
  header h1 { margin: 0; font-size: 20px; }
  .content { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; padding: 16px 20px 24px; }
  .card { border: 1px solid #eef2f7; border-radius: 8px; padding: 12px; background: #fafbfc; }
  label { display:block; font-size: 13px; color:#334155; margin: 8px 0 6px; }
  input[type=text], textarea, select { width: 100%; box-sizing: border-box; padding: 10px 12px; border: 1px solid #cbd5e1; border-radius: 6px; outline: none; font-size: 13px; background: #fff; }
  textarea { min-height: 120px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; }
  .row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  .row-3 { display: grid; grid-template-columns: 1fr 1fr auto; gap: 8px; align-items: end; }
  .actions { display:flex; gap: 12px; margin-top: 12px; }
  button { cursor: pointer; padding: 6px 10px; border-radius: 6px; border: 1px solid #2563eb; background: #2563eb; color: #fff; font-weight: 600; white-space: nowrap; }
  button.secondary { background: #fff; color: #1f2937; border-color: #cbd5e1; }
  button.danger { background: #b91c1c; border-color: #b91c1c; }
  .muted { color:#64748b; font-size: 12px; }
  pre { white-space: pre-wrap; word-break: break-word; background: #0b1021; color: #d1e7ff; padding: 12px; border-radius: 6px; font-size: 12px; line-height: 1.5; }
  .status { font-size: 13px; color:#0f172a; }
  .tweak-item { border: 1px dashed #cbd5e1; border-radius: 8px; padding: 10px; background: #fff; margin-top: 10px; }
  /* 层级更清晰的容器/卡片样式 */
  .container-card { border: 1px solid #e5e7eb; border-radius: 10px; padding: 12px; margin-top: 12px; background: #ffffff; position: relative; }
  .container-header { display:flex; align-items: end; justify-content: space-between; gap: 12px; padding-bottom: 8px; border-bottom: 1px dashed #e2e8f0; flex-wrap: nowrap; }
  .container-title { display:flex; align-items:center; gap:8px; flex-shrink: 0; }
  .badge { background:#eef2ff; color:#3730a3; font-weight:700; padding:2px 6px; border-radius:999px; font-size:11px; border:1px solid #c7d2fe; flex-shrink: 0; }
  .ct-name { flex-grow: 1; min-width: 100px; }
  .container-actions { display:flex; gap:6px; flex-wrap: nowrap; }
  .keys-wrap { margin-top: 10px; padding-left: 14px; border-left: 3px solid #e5e7eb; }
  .key-card.tweak-item { background:#fafbff; border-color:#dbeafe; }
  .key-card .tw-value { background:#ffffff; }
  .key-card.tweak-item button { font-size: 12px; padding: 4px 8px; }
  .key-card.tweak-item label { margin-bottom: 4px; }
  .key-card.tweak-item input, .key-card.tweak-item select, .key-card.tweak-item textarea { padding: 8px 10px; font-size: 12px; }
  .key-card.tweak-item .tw-type-custom { margin-top: 4px !important; }
  .tree-connector { position:absolute; left:8px; top:42px; bottom:12px; width:3px; background:linear-gradient(#e5e7eb,#e5e7eb); border-radius:2px; display:none; }
  .container-card.has-keys .tree-connector { display:block; }
  .ct-collapsed .keys-wrap { display:none; }
  header { position: relative; }
  .nav-link { position: absolute; top: 16px; right: 20px; background: #2563eb; color: #fff; padding: 8px 16px; border-radius: 6px; text-decoration: none; font-weight: 600; box-shadow: 0 2px 8px rgba(37,99,235,0.3); }
  /* Markdown 样式 */
  .markdown-output { 
    position: relative;
    background: #f8f9fa; 
    padding: 12px; 
    border-radius: 6px; 
    font-size: 13px; 
    line-height: 1.6; 
    border: 1px solid #e9ecef;
    max-height: 400px;
  }
  
  .markdown-content-wrapper {
    max-height: 350px;
    overflow-y: auto;
    white-space: pre-wrap;
    word-break: break-word;
    /* 移除开头空格 */
      margin: 0;
   }
   
   /* 确保内容区域没有默认空白 */
   .markdown-content {
     margin: 0;
     padding: 0;
     line-height: 1.6;
   }
   
   .markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content h5, .markdown-content h6 {
     margin-top: 16px;
    margin-bottom: 8px;
    font-weight: 600;
    line-height: 1.25;
  }
  .markdown-content h1 { font-size: 1.5em; }
  .markdown-content h2 { font-size: 1.3em; }
  .markdown-content h3 { font-size: 1.1em; }
  .markdown-content p { margin-bottom: 8px; }
  .markdown-content ul, .markdown-content ol { margin-bottom: 8px; padding-left: 20px; }
  .markdown-content li { margin-bottom: 4px; }
  .markdown-content code {
    background: rgba(175, 184, 193, 0.2);
    padding: 0.2em 0.4em;
    border-radius: 3px;
    font-size: 85%;
  }
  .markdown-content pre {
    background: #f6f8fa;
    border-radius: 6px;
    padding: 12px;
    overflow: auto;
    font-size: 85%;
    line-height: 1.45;
  }
  .markdown-content pre code {
    background: transparent;
    padding: 0;
  }
  .markdown-content blockquote {
    border-left: 4px solid #dfe2e5;
    padding-left: 16px;
    margin: 0 0 8px 0;
    color: #6a737d;
  }
  .markdown-content table {
    border-collapse: collapse;
    width: 100%;
    margin-bottom: 8px;
  }
  .markdown-content th, .markdown-content td {
    border: 1px solid #dfe2e5;
    padding: 6px 8px;
    text-align: left;
  }
  .markdown-content th {
    background: #f6f8fa;
    font-weight: 600;
  }
  .markdown-content a {
    color: #0366d6;
    text-decoration: none;
  }
  .markdown-content a:hover {
    text-decoration: underline;
  }
  .markdown-toggle {
    background-color: #f1f5f9;
    border: 1px solid #e2e8f0;
    color: #475569;
    padding: 4px 12px;
    border-radius: 4px;
    font-size: 12px;
    cursor: pointer;
    margin-top: 8px;
    transition: background-color 0.2s;
    /* 确保按钮固定在容器底部，不随内容滚动 */
    position: static;
  }
  
  .markdown-toggle:hover {
    background-color: #e2e8f0;
  }
  
  /* 响应式设计 - 确保在不同设备上正确显示 */
  @media (max-width: 768px) {
    .markdown-output {
      padding: 12px;
      font-size: 14px;
    }
    
    .markdown-output h1 { font-size: 20px; }
    .markdown-output h2 { font-size: 18px; }
    .markdown-output h3 { font-size: 16px; }
    .markdown-output h4 { font-size: 15px; }
    
    .markdown-output pre {
      padding: 12px;
      overflow-x: auto;
      font-size: 12px;
    }
    
    .markdown-output table {
      font-size: 12px;
      display: block;
      overflow-x: auto;
      white-space: nowrap;
    }
    
    .markdown-output ul, .markdown-output ol {
      padding-left: 20px;
    }
    
    .markdown-toggle {
      font-size: 11px;
      padding: 3px 10px;
    }
  }
  
  @media (max-width: 480px) {
    .markdown-output {
      padding: 10px;
      font-size: 13px;
    }
    
    .markdown-output h1 { font-size: 18px; }
    .markdown-output h2 { font-size: 16px; }
    .markdown-output h3 { font-size: 15px; }
    .markdown-output h4 { font-size: 14px; }
    
    .markdown-output pre {
      padding: 10px;
      font-size: 11px;
    }
    
    .markdown-output table {
      font-size: 11px;
    }
    
    .markdown-output ul, .markdown-output ol {
      padding-left: 15px;
    }
    
    .markdown-toggle {
      font-size: 10px;
      padding: 2px 8px;
    }
  }
  .markdown-collapsed .markdown-content-wrapper {
    max-height: 100px;
    overflow: hidden;
    position: relative;
  }
  
  .markdown-collapsed .markdown-content-wrapper:after {
    content: "";
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 20px;
    background: linear-gradient(transparent, #f8f9fa);
  }
  
  /* 删除旧的样式 */
  .markdown-collapsed .markdown-content {
    max-height: 100px;
    overflow: hidden;
    position: relative;
  }
  
  .markdown-collapsed .markdown-content:after {
    content: "";
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 20px;
    background: linear-gradient(transparent, #f8f9fa);
  }
</style>
</head>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<body>
  <div class=\"container\">
    <header>
      <h1>LangFlow API 测试工具</h1>
      <div class=\"muted\">输入参数后点击“发送请求”。返回：调用耗时与流程输出内容。</div>
    </header>
    <div class=\"content\">
      <div class=\"card\">
        <label>URL</label>
        <input id=\"url\" type=\"text\" value=\"__URL__\" />
        <div class=\"row\" style=\"margin-top:8px;\">
          <div>
            <label>session_id</label>
            <input id=\"session_id\" type=\"text\" value=\"__SESSION__\" />
          </div>
          <div>
            <label>Headers.x-api-key（可选）</label>
            <input id=\"api_key\" type=\"text\" placeholder=\"如果网关需要，填写 x-api-key\" />
          </div>
        </div>
        <label style=\"margin-top:8px;\">input_value（多行文本）</label>
        <textarea id=\"input_value\" placeholder=\"在此粘贴病历原文或任意输入\"></textarea>
        
        <div style=\"margin-top:10px;\">
          <label>
            <input type=\"checkbox\" id=\"stream_output\" /> 启用流式输出
          </label>
        </div>
        <div class=\"actions\">
          <button id=\"add_container\" class=\"secondary\">+ 添加组件名</button>
          <button id=\"clear_containers\" class=\"danger\">清空所有组件名</button>
        </div>
        <div id=\"containers\"></div>

        <div class=\"actions\" style=\"margin-top:14px;\">
          <button id=\"send\">发送请求</button>
          <button class=\"secondary\" id=\"reset\">重置</button>
          <button class=\"secondary\" id=\"copy_curl\">复制为CURL</button>
        </div>
        <div class=\"status\" id=\"status\"></div>
      </div>

      <div class=\"card\">
        <label>调用耗时（秒）</label>
        <input id=\"duration\" type=\"text\" readonly />
        <label style=\"margin-top:8px;\">流程输出内容</label>
        <div id=\"output\" class=\"markdown-output\">
          <div class=\"markdown-content-wrapper\">
            <div class=\"markdown-content\"></div>
          </div>
          <div class=\"markdown-toggle\" style=\"display: none;\">展开/收起</div>
        </div>
      </div>
    </div>
  </div>

<script>
const $ = (id) => document.getElementById(id);
const DEFAULT_CONTAINER_NAME = "__DEFAULT_CONTAINER_NAME__";

// 页面加载时清理输出内容框，确保没有预设空格
document.addEventListener('DOMContentLoaded', () => {
  const contentDiv = document.querySelector('.markdown-content');
  if (contentDiv) {
    // 确保内容为空且没有空白文本节点
    contentDiv.innerHTML = '';
  }
});

function createKeyItem(name = "__DEFAULT_ENTRY_NAME__", type = "__DEFAULT_ENTRY_TYPE__", value = `__DEFAULT_ENTRY_VALUE__`) {
  const wrap = document.createElement('div');
  wrap.className = 'tweak-item key-card';
  wrap.innerHTML = `
    <div class="row-3">
      <div>
        <label>键名</label>
        <input type="text" class="tw-name" value="${name}" />
      </div>
      <div>
        <label>值类型</label>
        <select class="tw-type">
          <option value="NestedDict">NestedDict</option>
          <option value="str">str</option>
          <option value="int">int</option>
          <option value="float">float</option>
          <option value="bool">bool</option>
          <option value="list">list</option>
          <option value="connect">connect</option>
          <option value="custom">自定义</option>
        </select>
        <input type="text" class="tw-type-custom" placeholder="自定义类型名" style="display:none;margin-top:8px;" />
      </div>
      <div style="display:flex;align-items:flex-end;gap:8px;">
        <button type="button" class="secondary tw-fill-demo">填入示例</button>
        <button type="button" class="danger tw-remove">删除键名</button>
      </div>
    </div>
    <label style="margin-top:8px;">值</label>
    <textarea class="tw-value">${value}</textarea>
  `;
  const typeSel = wrap.querySelector('.tw-type');
  const typeCus = wrap.querySelector('.tw-type-custom');
  typeSel.value = type;
  typeCus.style.display = (type === 'custom') ? 'block' : 'none';
  typeSel.addEventListener('change', () => {
    typeCus.style.display = (typeSel.value === 'custom') ? 'block' : 'none';
  });
  wrap.querySelector('.tw-remove').onclick = () => wrap.remove();
  wrap.querySelector('.tw-fill-demo').onclick = () => {
    wrap.querySelector('.tw-value').value = `__DEFAULT_ENTRY_VALUE__`;
  };
  return wrap;
}

function createContainer(containerName = DEFAULT_CONTAINER_NAME) {
  const box = document.createElement('div');
  box.className = 'container-card';
  box.innerHTML = `
    <div class="tree-connector"></div>
    <div class="container-header">
      <div class="container-title">
        <span class="badge">组件名</span>
        <input type="text" class="ct-name" value="${containerName}" />
      </div>
      <div class="container-actions">
        <button type="button" class="secondary ct-add-key">+ 添加键名</button>
        <button type="button" class="secondary ct-toggle">折叠/展开</button>
        <button type="button" class="danger ct-remove">删除组件名</button>
      </div>
    </div>
    <div class="keys-wrap ct-keys"></div>
  `;
  const keysWrap = box.querySelector('.ct-keys');
  const addKeyBtn = box.querySelector('.ct-add-key');
  const removeBtn = box.querySelector('.ct-remove');
  const toggleBtn = box.querySelector('.ct-toggle');

  const updateHasKeys = () => {
    if (keysWrap.children.length > 0) box.classList.add('has-keys');
    else box.classList.remove('has-keys');
  };

  const appendKey = () => {
    const item = createKeyItem();
    keysWrap.appendChild(item);
    const rm = item.querySelector('.tw-remove');
    if (rm) rm.addEventListener('click', () => setTimeout(updateHasKeys, 0));
    updateHasKeys();
  };

  addKeyBtn.onclick = appendKey;
  removeBtn.onclick = () => box.remove();
  toggleBtn.onclick = () => { box.classList.toggle('ct-collapsed'); };

  // 默认放一条键名，并设置 has-keys 状态
  appendKey();
  return box;
}

function collectTweaks() {
  const ret = {};
  const containers = document.querySelectorAll('#containers > div');
  for (const c of containers) {
    let cname = c.querySelector('.ct-name').value.trim();
    if (!cname) cname = DEFAULT_CONTAINER_NAME;
    const inner = {};
    const items = c.querySelectorAll('.tweak-item');
    for (const item of items) {
      const name = item.querySelector('.tw-name').value.trim();
      const typeSel = item.querySelector('.tw-type').value;
      const typeCus = item.querySelector('.tw-type-custom').value.trim();
      const valueText = item.querySelector('.tw-value').value || '';
      if (!name) continue;
      const t = (typeSel === 'custom') ? (typeCus || 'custom') : typeSel;
      let valueOut;
      if (t === 'NestedDict' || t === 'Dict') {
        try {
          valueOut = valueText ? JSON.parse(valueText) : {};
        } catch (e) {
          throw new Error(`组件名[${cname}] - 键名[${name}] 的值不是合法 JSON（类型: ${t}）`);
        }
      } else {
        valueOut = valueText;
      }
      inner[name] = { type: t, value: valueOut };
    }
    ret[cname] = inner;
  }
  return ret;
}

function resetTweaksToDefault() {
  const containers = document.getElementById('containers');
  containers.innerHTML = '';
  containers.appendChild(createContainer());
}

$("add_container").onclick = () => {
  document.getElementById('containers').appendChild(createContainer(DEFAULT_CONTAINER_NAME));
};
$("clear_containers").onclick = () => { document.getElementById('containers').innerHTML = ''; };

$("reset").onclick = () => {
  $("url").value = "__URL__";
  $("session_id").value = "__SESSION__";
  $("api_key").value = "";
  $("input_value").value = "";
  resetTweaksToDefault();
  $("duration").value = "";
  
  // 重置输出区域
  const outputDiv = $("output");
  const contentDiv = outputDiv.querySelector(".markdown-content");
  const toggleBtn = outputDiv.querySelector(".markdown-toggle");
  
  contentDiv.innerHTML = "";
  outputDiv.classList.remove("markdown-collapsed");
  toggleBtn.textContent = "展开/收起";
  toggleBtn.style.display = "none";
  
  $("status").textContent = "";
};

// 初始化默认组件名与键名
resetTweaksToDefault();

$("send").onclick = async () => {
  let tweaks;
  try {
    tweaks = collectTweaks();
  } catch (e) {
    $("status").textContent = e.message;
    return;
  }
  const url = $("url").value.trim();
  const session_id = $("session_id").value.trim() || "__SESSION__";
  const api_key = $("api_key").value.trim();
  const input_value = $("input_value").value;
  const stream_output = $("stream_output").checked;

  $("status").textContent = "请求中…";
  $("duration").value = "";
  
  // 清空输出区域
    const outputDiv = $("output");
    const contentDiv = outputDiv.querySelector(".markdown-content");
    const toggleBtn = outputDiv.querySelector(".markdown-toggle");
    
    contentDiv.innerHTML = "";
    outputDiv.classList.remove("markdown-collapsed");
    toggleBtn.textContent = "展开/收起";
    toggleBtn.style.display = "none";

  try {
    if (stream_output) {
      // 使用流式API端点
      const startTime = Date.now();
      const response = await fetch("/api/invoke-stream", {
        method: "POST",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify({ url, session_id, input_value, tweaks, api_key })
      });
      
      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.detail || "请求失败");
      }
      
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let buffer = "";
      let fullText = "";
      
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        
        buffer += decoder.decode(value, { stream: true });
        // 使用双换行符分割SSE事件
        const lines = buffer.split('\\n\\n');
        buffer = lines.pop(); // 保留不完整的行
        
        for (const line of lines) {
          if (!line.trim() || !line.startsWith('data: ')) continue;
          
          try {
            const dataStr = line.slice(6); // 移除 "data: "
            const parsed = JSON.parse(dataStr);
            
            // 检查是否有错误
            if (parsed.error) {
              $("status").textContent = `错误：${parsed.error}`;
              continue;
            }
            
            // 检查是否完成
            if (parsed.done) {
              if (parsed.duration !== undefined) {
                $("duration").value = parsed.duration.toFixed(2);
              }
              $("status").textContent = "完成";
              continue;
            }
            
            // 处理文本内容
            if (parsed.text !== undefined) {
              // 保留最新的完整文本并去除开头空格
              fullText = parsed.text.trim();
              
              // 确保即使内容为空也正确设置
              const markdownHtml = marked.parse(fullText || '');
              contentDiv.innerHTML = markdownHtml;
              
              // 移除可能存在的空白节点
              Array.from(contentDiv.childNodes).forEach(node => {
                if (node.nodeType === 3 && node.textContent.trim() === '') {
                  node.parentNode.removeChild(node);
                }
              });
              
              // 确保内容可见
              outputDiv.classList.remove("markdown-collapsed");
              toggleBtn.textContent = "收起";
              toggleBtn.style.display = "block";
              
              // 添加点击事件处理
              toggleBtn.onclick = function() {
                if (outputDiv.classList.contains("markdown-collapsed")) {
                  outputDiv.classList.remove("markdown-collapsed");
                  toggleBtn.textContent = "收起";
                } else {
                  outputDiv.classList.add("markdown-collapsed");
                  toggleBtn.textContent = "展开";
                }
              };
            }
          } catch (e) {
            console.error("解析流数据错误:", e, line);
          }
        }
      }
      
      const endTime = Date.now();
      $("duration").value = ((endTime - startTime) / 1000).toFixed(2);
      $("status").textContent = "完成";
    } else {
      // 使用原有的非流式API端点
      const resp = await fetch("/api/invoke", {
        method: "POST",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify({ url, session_id, input_value, tweaks, api_key })
      });
      const data = await resp.json();
      if (!resp.ok) throw new Error(data.detail || "请求失败");
      $("duration").value = data.duration.toFixed(2);
      
      // 渲染markdown内容
      const outputDiv = $("output");
      // 直接获取子元素，确保正确找到
      const contentDiv = outputDiv.querySelector(".markdown-content");
      const toggleBtn = outputDiv.querySelector(".markdown-toggle");
      
      if (data.output_text) {
          // 使用marked.js将markdown转换为HTML，并添加空值检查
          const markdownHtml = marked.parse(data.output_text || '');
          contentDiv.innerHTML = markdownHtml;
          
          // 确保内容可见
          outputDiv.classList.remove("markdown-collapsed");
          toggleBtn.textContent = "收起";
          toggleBtn.style.display = "block";
          
          // 添加点击事件处理
          toggleBtn.onclick = function() {
            if (outputDiv.classList.contains("markdown-collapsed")) {
              outputDiv.classList.remove("markdown-collapsed");
              toggleBtn.textContent = "收起";
            } else {
              outputDiv.classList.add("markdown-collapsed");
              toggleBtn.textContent = "展开";
            }
          };
        } else {
          contentDiv.innerHTML = "";
          outputDiv.classList.remove("markdown-collapsed");
          toggleBtn.textContent = "展开/收起";
          toggleBtn.style.display = "none";
        }
      
      $("status").textContent = "完成";
    }
  } catch (err) {
    $("status").textContent = "错误：" + err.message;
  }
};

function replaceAll(s, search, replacement) {
  return String(s).split(search).join(replacement);
}

function escapeForDoubleQuoted(str) {
  let s = String(str);
  s = replaceAll(s, "\\", "\\\\");
  s = replaceAll(s, '"', '\\"');
  return s;
}

async function copyText(text) {
  if (navigator.clipboard && window.isSecureContext) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch (e) {
      return false;
    }
  } else {
    const ta = document.createElement('textarea');
    ta.value = text;
    ta.style.position = 'fixed';
    ta.style.top = '-1000px';
    document.body.appendChild(ta);
    ta.focus();
    ta.select();
    let ok = false;
    try {
      ok = document.execCommand('copy');
    } catch (e) {
      ok = false;
    }
    document.body.removeChild(ta);
    return ok;
  }
}

$("copy_curl").onclick = () => {
  let tweaks;
  try {
    tweaks = collectTweaks();
  } catch (e) {
    $("status").textContent = e.message;
    return;
  }

  const url = $("url").value.trim();
  const session_id = $("session_id").value.trim() || "__SESSION__";
  const api_key = $("api_key").value.trim();
  const input_value = $("input_value").value;
  const stream_output = $("stream_output").checked;

  const body = {
    input_value,
    output_type: "chat",
    input_type: "chat",
    session_id,
    tweaks
  };

  let targetUrl = url;
  if (stream_output) {
    targetUrl = url.includes("?") ? url + "&stream=true" : url + "?stream=true";
  }

  const headers = [];
  headers.push("-H \"Content-Type: application/json\"");
  headers.push(`-H \"accept: ${stream_output ? "text/event-stream" : "application/json"}\"`);
  if (api_key) headers.push(`-H \"x-api-key: ${escapeForDoubleQuoted(api_key)}\"`);

  const jsonStr = JSON.stringify(body);
  const escapedJson = escapeForDoubleQuoted(jsonStr);

  const parts = [];
  parts.push("curl");
  if (stream_output) parts.push("-N");
  parts.push("-sS");
  parts.push("-X POST");
  parts.push(`\"${escapeForDoubleQuoted(targetUrl)}\"`);
  parts.push(...headers);
  parts.push(`-d \"${escapedJson}\"`);

  const cmd = parts.join(" ");

  copyText(cmd).then((ok) => {
    if (ok) {
      $("status").textContent = "已复制 CURL 命令到剪贴板";
    } else {
      $("status").textContent = "复制失败，请手动复制";
      try { window.prompt("CURL 命令", cmd); } catch (_) {}
    }
  });
};
</script>
</body>
</html>
"""

@app.get("/", response_class=HTMLResponse)
def index(request: Request):
    default_container = DEFAULT_CONTAINER_NAME
    default_entry_name = "my_json_schema"
    default_entry_type = "NestedDict"
    default_entry_value = DEFAULT_TWEAKS[default_container][default_entry_name]["value"]
    return templates.TemplateResponse("index.html", {
        "request": request,
        "default_url": DEFAULT_URL,
        "default_session_id": DEFAULT_SESSION_ID,
        "default_container_name": default_container,
        "default_entry_name": default_entry_name,
        "default_entry_type": default_entry_type,
        "default_entry_value": default_entry_value,
        "env_api_key": os.getenv("LANGFLOW_API_KEY")
    })

@app.post("/api/invoke")
def api_invoke(payload: dict):
    url = payload.get("url") or DEFAULT_URL
    session_id = payload.get("session_id") or DEFAULT_SESSION_ID
    input_value = payload.get("input_value", "")
    tweaks = payload.get("tweaks") or {}
    api_key = payload.get("api_key") or None

    # 来自 .env 的默认 API key（可被前端传入覆盖）
    env_api_key = os.getenv("LANGFLOW_API_KEY")
    headers = {"Content-Type": "application/json", "accept": "application/json"}
    # 优先使用用户传入的api_key参数，再使用环境变量中的env_api_key
    if api_key:
        headers["x-api-key"] = api_key
    elif env_api_key:
        headers["x-api-key"] = env_api_key
    body = {
        "input_value": input_value,
        "output_type": "chat",
        "input_type": "chat",
        "session_id": session_id,
        "tweaks": tweaks,
    }

    start = time.time()
    try:
        resp = requests.post(url, json=body, headers=headers, timeout=DEFAULT_TIMEOUT)
        status = resp.status_code
        resp_text = resp.text
        if status >= 400:
            # 将上游错误原文透传，便于定位（截断到 2000 字符防止过长）
            raise HTTPException(status_code=status, detail=f"上游返回 {status}: {resp_text[:2000]}")
        data = resp.json()
        # 兼容当前示例数据路径
        text = (
            data.get("outputs", [{}])[0]
                .get("outputs", [{}])[0]
                .get("results", {})
                .get("message", {})
                .get("data", {})
                .get("text", "")
        )
        # 移除开头的空格
        text = text.strip()
    except requests.RequestException as e:
        raise HTTPException(status_code=502, detail=f"上游请求错误: {e}")
    except ValueError as e:
        raise HTTPException(status_code=500, detail=f"解析响应失败: {e}")
    finally:
        duration = time.time() - start

    return JSONResponse({"duration": duration, "output_text": text})

@app.post("/api/invoke-stream")
async def api_invoke_stream(payload: dict):
    url = payload.get("url") or DEFAULT_URL
    session_id = payload.get("session_id") or DEFAULT_SESSION_ID
    input_value = payload.get("input_value", "")
    tweaks = payload.get("tweaks") or {}
    api_key = payload.get("api_key") or None

    # 来自 .env 的默认 API key（可被前端传入覆盖）
    env_api_key = os.getenv("LANGFLOW_API_KEY")
    headers = {"Content-Type": "application/json", "accept": "application/json"}
    # 优先使用用户传入的api_key参数，再使用环境变量中的env_api_key
    if api_key:
        headers["x-api-key"] = api_key
    elif env_api_key:
        headers["x-api-key"] = env_api_key
    
    body = {
        "input_value": input_value,
        "output_type": "chat",
        "input_type": "chat",
        "session_id": session_id,
        "tweaks": tweaks,
    }

    # 添加stream=true参数到URL
    if "?" in url:
        stream_url = url + "&stream=true"
    else:
        stream_url = url + "?stream=true"
    
    start = time.time()
    
    def generate():
        try:
            # 使用流式请求
            resp = requests.post(stream_url, json=body, headers=headers, stream=True, timeout=DEFAULT_TIMEOUT)
            
            if resp.status_code >= 400:
                error_text = resp.text
                yield f"data: {json.dumps({'error': f'上游返回 {resp.status_code}: {error_text[:2000]}'})}\n\n"
                return
            
            # 处理流式响应 - LangFlow API返回的是每行一个JSON对象
            buffer = ""
            full_text = ""  # 初始化累积文本变量
            
            for chunk in resp.iter_content(chunk_size=1024):
                if chunk:
                    try:
                        buffer += chunk.decode('utf-8', errors='ignore')
                    except Exception as e:
                        yield f"data: {{\"error\": \"解码错误: {str(e)}\"}}\n\n"
                        continue
                    
                    # 按行处理，每行应该是一个JSON对象
                    lines = buffer.split('\n')
                    buffer = lines.pop()  # 保留不完整的行
                    
                    for line in lines:
                        line = line.strip()
                        if not line:
                            continue
                            
                        try:
                            # 解析每一行的JSON
                            data = json.loads(line)
                            
                            # 处理token事件，提取文本内容
                            if data.get("event") == "token" and "data" in data:
                                token_data = data["data"]
                                if "chunk" in token_data:
                                    chunk_text = token_data["chunk"]
                                    full_text += chunk_text
                                    
                                    # 日志：控制台输出每个片段
                                    try:
                                        print(f"[STREAM CHUNK] {chunk_text}")
                                    except Exception:
                                        pass
                                    # 日志：追加到文件
                                    try:
                                        with open('app_stream.log', 'a', encoding='utf-8') as lf:
                                            lf.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - CHUNK: {chunk_text}\n")
                                    except Exception:
                                        pass

                                    # 发送累积的文本
                                    yield f"data: {json.dumps({'text': full_text, 'done': False})}\n\n"
                                    
                        except json.JSONDecodeError:
                            # 忽略无法解析的行
                            continue
                        except Exception as parse_error:
                            yield f'data: {{"error": "处理数据错误: {str(parse_error)}"}}\n\n'
            
            # 发送完成信号
            duration = time.time() - start
            yield f"data: {{\"done\": true, \"duration\": {duration}}}\n\n"
                
        except Exception as e:
            yield f'data: {{"error": "请求错误: {str(e)}"}}\n\n'
    
    return StreamingResponse(generate(), media_type="text/plain")

# 批量测试页面HTML模板
BATCH_HTML_TEMPLATE = """
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>LangFlow 批量测试工具</title>
<style>
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'PingFang SC', 'Microsoft YaHei', sans-serif; margin: 0; padding: 0; background: #f6f7f9; }
  .container { max-width: 1400px; margin: 24px auto; background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.04); }
  header { padding: 16px 20px; border-bottom: 1px solid #eef2f7; display: flex; justify-content: space-between; align-items: center; }
  header h1 { margin: 0; font-size: 20px; }
  .nav-btn { background: #10b981; color: #fff; padding: 8px 16px; border-radius: 6px; text-decoration: none; font-weight: 600; box-shadow: 0 2px 8px rgba(16,185,129,0.3); }
  .content { padding: 20px; }
  .card { border: 1px solid #eef2f7; border-radius: 8px; padding: 16px; background: #fafbfc; margin-bottom: 16px; }
  label { display: block; font-size: 13px; color: #334155; margin: 8px 0 6px; font-weight: 600; }
  input[type=text], input[type=file] { width: 100%; box-sizing: border-box; padding: 10px 12px; border: 1px solid #cbd5e1; border-radius: 6px; outline: none; font-size: 13px; background: #fff; }
  input[type=radio] { margin-right: 6px; }
  .radio-group { display: flex; gap: 20px; margin-top: 8px; }
  .radio-item { display: flex; align-items: center; }
  button { cursor: pointer; padding: 10px 20px; border-radius: 6px; border: 1px solid #2563eb; background: #2563eb; color: #fff; font-weight: 600; font-size: 14px; }
  button:disabled { background: #94a3b8; border-color: #94a3b8; cursor: not-allowed; }
  button.secondary { background: #fff; color: #1f2937; border-color: #cbd5e1; }
  button.success { background: #10b981; border-color: #10b981; }
  .muted { color: #64748b; font-size: 12px; margin-top: 4px; }
  .progress { margin-top: 12px; }
  .progress-bar { width: 100%; height: 24px; background: #e5e7eb; border-radius: 12px; overflow: hidden; }
  .progress-fill { height: 100%; background: linear-gradient(90deg, #2563eb, #3b82f6); transition: width 0.3s; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 12px; font-weight: 600; }
  .result-table { width: 100%; border-collapse: collapse; margin-top: 12px; font-size: 13px; }
  .result-table th { background: #f1f5f9; padding: 10px; text-align: left; border: 1px solid #e2e8f0; font-weight: 600; position: sticky; top: 0; }
  .result-table td { padding: 8px 10px; border: 1px solid #e2e8f0; vertical-align: top; max-width: 300px; word-break: break-word; }
  .result-table tr:hover { background: #f8fafc; }
  .status-success { color: #10b981; font-weight: 600; }
  .status-error { color: #ef4444; font-weight: 600; }
  .table-wrapper { max-height: 600px; overflow: auto; margin-top: 12px; border: 1px solid #e2e8f0; border-radius: 6px; }
  pre { white-space: pre-wrap; word-break: break-word; margin: 0; font-size: 12px; font-family: ui-monospace, monospace; }
  .actions { display: flex; gap: 12px; margin-top: 12px; }
  .info-box { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 6px; padding: 12px; margin-top: 12px; }
  .info-box h3 { margin: 0 0 8px 0; font-size: 14px; color: #1e40af; }
  .info-box ul { margin: 4px 0; padding-left: 20px; font-size: 12px; color: #1e40af; }
  .text-cell { display: flex; flex-direction: column; gap: 6px; }
  .text-cell .text-content { margin: 0; max-height: 220px; overflow: hidden; }
  .text-cell.expanded .text-content { max-height: none; }
  .text-cell .text-toggle { align-self: flex-start; padding: 4px 10px; border-radius: 4px; border: 1px solid #93c5fd; background: #eff6ff; color: #1d4ed8; font-size: 12px; cursor: pointer; user-select: none; }
  .text-cell .text-toggle:hover { background: #dbeafe; }
  .text-cell .text-collapsed { display: inline; }
  .text-cell .text-full { display: none; }
  .text-cell.expanded .text-collapsed { display: none; }
  .text-cell.expanded .text-full { display: inline; }
</style>
</head>
<body>
  <div class="container">
    <header>
      <h1>LangFlow 批量测试工具</h1>
      <a href="/" class="nav-btn">← 单次测试</a>
    </header>
    <div class="content">
      <div class="card">
        <label>LangFlow API URL</label>
        <input id="url" type="text" value="__URL__" />
        
        <label style="margin-top: 12px;">session_id 模式</label>
        <div class="radio-group">
          <div class="radio-item">
            <input type="radio" id="session_auto" name="session_mode" value="auto" checked />
            <label for="session_auto" style="margin: 0;">自动生成（每个请求唯一）</label>
          </div>
          <div class="radio-item">
            <input type="radio" id="session_manual" name="session_mode" value="manual" />
            <label for="session_manual" style="margin: 0;">手动输入</label>
          </div>
        </div>
        <input id="session_value" type="text" placeholder="手动输入 session_id（选择手动模式时使用）" style="margin-top: 8px;" />
        <div class="muted">优先级：表格中的 session_id 列 > 此处设置的值</div>
        
        <label style="margin-top: 12px;">上传测试数据表格</label>
        <input id="file" type="file" accept=".xlsx,.xls,.csv" />
        <div class="muted">支持 Excel (.xlsx, .xls) 和 CSV (.csv) 格式</div>
        
        <div class="actions">
          <button id="start_test">开始批量测试</button>
          <button id="export_result" class="success" style="display:none;">导出结果</button>
          <button id="download_template" class="secondary">下载表格模板</button>
        </div>
        
        <div id="progress" class="progress" style="display:none;">
          <div class="progress-bar">
            <div id="progress_fill" class="progress-fill" style="width: 0%;">0%</div>
          </div>
          <div class="muted" style="margin-top: 8px;" id="progress_text">准备中...</div>
        </div>
      </div>
      
      <div class="info-box">
        <h3>📋 表格格式说明</h3>
        <ul>
          <li><strong>必需列</strong>：<code>input_value</code> - 输入文本内容</li>
          <li><strong>可选列</strong>：<code>session_id</code> - 会话ID（如果存在，优先使用此列的值）</li>
          <li><strong>组件参数列</strong>：格式为 <code>组件名.键名[类型]</code>，例如：
            <ul>
              <li><code>SchemaBlueprint.my_json_schema[NestedDict]</code> - 值为 JSON 字符串</li>
              <li><code>SchemaBlueprint.temperature[float]</code> - 值为数字</li>
            </ul>
          </li>
          <li><strong>支持的类型</strong>：str, int, float, bool, list, NestedDict, Dict 等</li>
          <li><strong>特殊字符</strong>：Excel 格式推荐，支持换行、引号等特殊字符</li>
        </ul>
      </div>
      
      <div class="card" id="result_card" style="display:none;">
        <h3 style="margin: 0 0 12px 0;">测试结果</h3>
        <div class="table-wrapper">
          <table class="result-table" id="result_table">
            <thead>
              <tr>
                <th>序号</th>
                <th>session_id</th>
                <th>input_value</th>
                <th>调用耗时(秒)</th>
                <th>流程输出内容</th>
                <th>状态</th>
              </tr>
            </thead>
            <tbody id="result_tbody"></tbody>
          </table>
        </div>
      </div>
    </div>
  </div>

<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<script>
const $ = (id) => document.getElementById(id);
let testResults = [];

$('start_test').onclick = async () => {
  const fileInput = $('file');
  if (!fileInput.files.length) {
    alert('请先选择要上传的表格文件');
    return;
  }
  
  const url = $('url').value.trim();
  const sessionMode = document.querySelector('input[name="session_mode"]:checked').value;
  const sessionValue = $('session_value').value.trim();
  
  const formData = new FormData();
  formData.append('file', fileInput.files[0]);
  formData.append('url', url);
  formData.append('session_mode', sessionMode);
  formData.append('session_value', sessionValue);
  
  $('start_test').disabled = true;
  $('progress').style.display = 'block';
  $('result_card').style.display = 'block';
  $('export_result').style.display = 'none';
  $('result_tbody').innerHTML = '';
  testResults = [];
  
  let total = 0;
  let processed = 0;
  
  try {
    const resp = await fetch('/api/batch-invoke', {
      method: 'POST',
      body: formData
    });
    
    if (!resp.ok) {
      const data = await resp.json();
      throw new Error(data.detail || '请求失败');
    }
    
    // 使用 EventSource 风格读取流
    const reader = resp.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    
    while (true) {
      const {done, value} = await reader.read();
      if (done) break;
      
      buffer += decoder.decode(value, {stream: true});
      const lines = buffer.split('\\n\\n');
      buffer = lines.pop(); // 保留不完整的行
      
      for (const line of lines) {
        if (!line.trim() || !line.startsWith('data: ')) continue;
        
        try {
          const jsonStr = line.substring(6); // 移除 "data: "
          const event = JSON.parse(jsonStr);
          
          if (event.type === 'total') {
            total = event.count;
            $('progress_text').textContent = `准备处理 ${total} 条数据...`;
          } else if (event.type === 'result') {
            processed++;
            testResults.push(event.data);
            
            // 动态添加结果行
            addResultRow(event.data);
            
            // 更新进度
            const percent = Math.round((processed / total) * 100);
            $('progress_fill').style.width = percent + '%';
            $('progress_fill').textContent = percent + '%';
            $('progress_text').textContent = `处理中 ${processed}/${total}`;
          } else if (event.type === 'done') {
            $('progress_fill').style.width = '100%';
            $('progress_fill').textContent = '100%';
            $('progress_text').textContent = `完成！共处理 ${testResults.length} 条数据`;
            $('export_result').style.display = 'inline-block';
          }
        } catch (e) {
          console.error('解析事件失败:', e, line);
        }
      }
    }
    
  } catch (err) {
    alert('错误：' + err.message);
    $('progress_text').textContent = '错误：' + err.message;
  } finally {
    $('start_test').disabled = false;
  }
};

// 创建可展开/折叠的文本单元格
function createExpandableCell(text, maxLength) {
  const fullText = String(text || '');
  if (fullText.length <= maxLength) {
    return `<pre>${escapeHtml(fullText)}</pre>`;
  }
  
  const previewText = fullText.substring(0, maxLength);
  const hiddenChars = fullText.length - maxLength;
  const cellId = 'cell_' + Math.random().toString(36).substring(2, 9);
  
  return `
    <div class="text-cell" id="${cellId}">
      <pre class="text-content">
<span class="text-collapsed">${escapeHtml(previewText)}</span><span class="text-full">${escapeHtml(fullText)}</span>
</pre>
      <button class="text-toggle" onclick="toggleTextCell('${cellId}')">...展开 (还有 ${hiddenChars} 个字符)</button>
    </div>
  `;
}

// 切换文本单元格展开/折叠状态
function toggleTextCell(cellId) {
  const cell = document.getElementById(cellId);
  const button = cell.querySelector('.text-toggle');
  const isExpanded = cell.classList.contains('expanded');
  
  if (isExpanded) {
    cell.classList.remove('expanded');
    const fullText = cell.querySelector('.text-full').textContent;
    const hiddenChars = fullText.length - 100;
    button.textContent = `...展开 (还有 ${hiddenChars} 个字符)`;
  } else {
    cell.classList.add('expanded');
    button.textContent = '...折叠';
  }
}

function addResultRow(row) {
  const tbody = $('result_tbody');
  const tr = document.createElement('tr');
  const idx = testResults.length;
  
  const inputCell = createExpandableCell(row.input_value, 100);
  const outputCell = createExpandableCell(row.output_text || row.error, 200);
  
  tr.innerHTML = `
    <td>${idx}</td>
    <td>${escapeHtml(row.session_id || '')}</td>
    <td>${inputCell}</td>
    <td>${row.duration !== null ? row.duration.toFixed(2) : '-'}</td>
    <td>${outputCell}</td>
    <td class="${row.status === 'success' ? 'status-success' : 'status-error'}">${row.status === 'success' ? '成功' : '失败'}</td>
  `;
  tbody.appendChild(tr);
  
  // 自动滚动到最新行
  tr.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}

function displayResults(results) {
  const tbody = $('result_tbody');
  tbody.innerHTML = '';
  
  results.forEach((row, idx) => {
    const tr = document.createElement('tr');
    
    const inputCell = createExpandableCell(row.input_value, 100);
    const outputCell = createExpandableCell(row.output_text || row.error, 200);
    
    tr.innerHTML = `
      <td>${idx + 1}</td>
      <td>${escapeHtml(row.session_id || '')}</td>
      <td>${inputCell}</td>
      <td>${row.duration !== null ? row.duration.toFixed(2) : '-'}</td>
      <td>${outputCell}</td>
      <td class="${row.status === 'success' ? 'status-success' : 'status-error'}">${row.status === 'success' ? '成功' : '失败'}</td>
    `;
    tbody.appendChild(tr);
  });
  
  $('result_card').style.display = 'block';
}

function escapeHtml(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

$('export_result').onclick = () => {
  const ws_data = [['序号', 'session_id', 'input_value', '调用耗时(秒)', '流程输出内容', '状态']];
  
  testResults.forEach((row, idx) => {
    ws_data.push([
      idx + 1,
      row.session_id || '',
      row.input_value || '',
      row.duration !== null ? row.duration : '',
      row.output_text || row.error || '',
      row.status === 'success' ? '成功' : '失败'
    ]);
  });
  
  const wb = XLSX.utils.book_new();
  const ws = XLSX.utils.aoa_to_sheet(ws_data);
  XLSX.utils.book_append_sheet(wb, ws, '测试结果');
  
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
  XLSX.writeFile(wb, `langflow_batch_result_${timestamp}.xlsx`);
};

$('download_template').onclick = () => {
  const ws_data = [
    ['session_id', 'input_value', 'SchemaBlueprint.my_json_schema[NestedDict]'],
    ['test_001', '患者姓名：张三，已婚', '{"schema":{"type":"object","properties":{"姓名":{"type":"string","description":"患者姓名"},"婚否":{"type":"string","description":"患者是否已婚"}},"required":["姓名"],"additionalProperties":false},"name":"jsonEntity","strict":true}'],
    ['test_002', '患者姓名：李四，未婚', '{"schema":{"type":"object","properties":{"姓名":{"type":"string","description":"患者姓名"},"婚否":{"type":"string","description":"患者是否已婚"}},"required":["姓名"],"additionalProperties":false},"name":"jsonEntity","strict":true}']
  ];
  
  const wb = XLSX.utils.book_new();
  const ws = XLSX.utils.aoa_to_sheet(ws_data);
  XLSX.utils.book_append_sheet(wb, ws, '测试数据');
  XLSX.writeFile(wb, 'langflow_batch_template.xlsx');
};
</script>
</body>
</html>
"""

@app.get("/batch", response_class=HTMLResponse)
def batch_test_page(request: Request):
    return templates.TemplateResponse("batch.html", {"request": request, "default_url": DEFAULT_URL})

def parse_column_name(col_name: str):
    """解析列名，提取组件名、键名和类型
    格式：组件名.键名[类型]
    例如：SchemaBlueprint.my_json_schema[NestedDict]
    """
    pattern_with_component = r'^(.+?)\.(.+?)\[(.+?)\]$'
    pattern_without_component = r'^(.+?)\[(.+?)\]$'

    match = re.match(pattern_with_component, col_name)
    if match:
        return {
            'component': match.group(1),
            'key': match.group(2),
            'type': match.group(3)
        }

    match_no_component = re.match(pattern_without_component, col_name)
    if match_no_component:
        return {
            'component': DEFAULT_CONTAINER_NAME,
            'key': match_no_component.group(1),
            'type': match_no_component.group(2)
        }
    return None

def convert_value_by_type(value, value_type: str):
    """根据类型转换值"""
    if pd.isna(value):
        return None
    
    value_str = str(value).strip()
    if not value_str:
        return None
    
    if value_type in ['NestedDict', 'Dict', 'dict', 'list']:
        try:
            return json.loads(value_str)
        except json.JSONDecodeError:
            raise ValueError(f"无法解析 JSON: {value_str[:100]}")
    elif value_type == 'int':
        return int(float(value_str))
    elif value_type == 'float':
        return float(value_str)
    elif value_type == 'bool':
        return value_str.lower() in ['true', '1', 'yes', 'y', '是']
    else:  # str 或其他
        return value_str

@app.post("/api/batch-invoke")
async def batch_invoke(
    file: UploadFile = File(...),
    url: str = Form(...),
    session_mode: str = Form(...),
    session_value: str = Form("")
):
    """批量调用 LangFlow API - 流式返回结果"""
    try:
        # 读取上传的文件
        content = await file.read()
        
        # 根据文件类型解析
        if file.filename.endswith(('.xlsx', '.xls')):
            df = pd.read_excel(BytesIO(content), engine='openpyxl')
        elif file.filename.endswith('.csv'):
            df = pd.read_csv(BytesIO(content), encoding='utf-8-sig')
        else:
            raise HTTPException(status_code=400, detail="仅支持 .xlsx, .xls, .csv 格式")
        
        # 检查必需列
        if 'input_value' not in df.columns:
            raise HTTPException(status_code=400, detail="表格必须包含 'input_value' 列")
        
        # 定义流式生成器
        async def generate_results():
            env_api_key = os.getenv("LANGFLOW_API_KEY")
            total = len(df)
            
            # 发送总数
            yield f"data: {json.dumps({'type': 'total', 'count': total}, ensure_ascii=False)}\n\n"

            
            for idx, row in df.iterrows():
                result = {
                    'row_number': idx + 1,
                    'session_id': None,
                    'input_value': None,
                    'duration': None,
                    'output_text': None,
                    'error': None,
                    'status': 'error'
                }
                
                try:
                    # 获取 input_value
                    input_value = row.get('input_value', '')
                    if pd.isna(input_value):
                        input_value = ''
                    result['input_value'] = str(input_value)
                    
                    # 确定 session_id
                    if 'session_id' in df.columns and not pd.isna(row['session_id']):
                        session_id = str(row['session_id'])
                    elif session_mode == 'manual' and session_value:
                        session_id = session_value
                    else:
                        session_id = f"batch_{idx+1}_{uuid.uuid4().hex[:8]}"
                    result['session_id'] = session_id
                    
                    # 构建 tweaks
                    tweaks = {}
                    for col in df.columns:
                        if col in ['session_id', 'input_value']:
                            continue
                        
                        parsed = parse_column_name(col)
                        if not parsed:
                            continue

                        component = parsed['component']
                        key = parsed['key']
                        value_type = parsed['type']

                        if component not in tweaks:
                            tweaks[component] = {}

                        try:
                            value = convert_value_by_type(row[col], value_type)
                            if value is not None:
                                tweaks[component][key] = {
                                    'type': value_type,
                                    'value': value
                                }
                        except Exception as e:
                            result['error'] = f"列 '{col}' 值转换失败: {str(e)}"
                            # 发送错误结果
                            yield f"data: {json.dumps({'type': 'result', 'data': result}, ensure_ascii=False)}\n\n"
                            continue

                    # 调用 LangFlow API
                    headers = {"Content-Type": "application/json", "accept": "application/json"}
                    if env_api_key:
                        headers["x-api-key"] = env_api_key
                    
                    body = {
                        "input_value": str(input_value),
                        "output_type": "chat",
                        "input_type": "chat",
                        "session_id": session_id,
                        "tweaks": tweaks,
                    }
                    
                    start = time.time()
                    resp = requests.post(url, json=body, headers=headers, timeout=DEFAULT_TIMEOUT)
                    duration = time.time() - start
                    result['duration'] = duration
                    
                    if resp.status_code >= 400:
                        result['error'] = f"API 返回错误 {resp.status_code}: {resp.text[:500]}"
                    else:
                        data = resp.json()
                        text = (
                            data.get("outputs", [{}])[0]
                                .get("outputs", [{}])[0]
                                .get("results", {})
                                .get("message", {})
                                .get("data", {})
                                .get("text", "")
                        )
                        # 移除开头的空格
                        text = text.strip()
                        result['output_text'] = text
                        result['status'] = 'success'
                        
                except Exception as e:
                    result['error'] = str(e)
                
                # 发送每一行的结果
                yield f"data: {json.dumps({'type': 'result', 'data': result}, ensure_ascii=False)}\n\n"
            
            # 发送完成信号
            yield f"data: {json.dumps({'type': 'done'}, ensure_ascii=False)}\n\n"
        
        # 返回流式响应
        return StreamingResponse(generate_results(), media_type="text/event-stream")
        
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"处理失败: {str(e)}")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)
